// This source code is subject to the terms of the Mozilla Public License 2.0
// at https://mozilla.org/MPL/2.0/
// © josseliani
// SMT-style intermarket stretch analysis with independent adaptive range oscillators.
//@version=6
indicator("SMT Divergence Oscillator [josseliani]", shorttitle="SMT Divergence", overlay=false, precision=2, max_labels_count=200)

// ─── Inputs ──────────────────────────────────────────────────────────────────
groupCompare = "Comparison"
indexMode = input.string("Synthetic USD", "Comparison source", options=["Synthetic USD", "Synthetic EUR", "Synthetic GBP", "Synthetic AUD", "Synthetic CAD", "Synthetic CHF", "Synthetic JPY", "Synthetic NZD", "Synthetic XAU", "Custom symbol"], group=groupCompare, tooltip="Select a synthetic currency-strength basket or another TradingView market.")
compareSymbol = input.symbol("TVC:DXY", "Custom symbol", group=groupCompare, tooltip="Symbol used in Custom symbol mode.")
invertMode = input.string("Auto", "Comparison orientation", options=["Auto", "Inverted", "Normal"], group=groupCompare, tooltip="Auto aligns the blue line with the chart when possible. Override if needed.")

groupSmooth = "Smoothing"
smoothLen = input.int(8, "Smoothing length", minval=4, maxval=100, group=groupSmooth, tooltip="Lower = faster, more signals and noise. Higher = smoother, slower.")
responsiveness = input.float(1.0, "Responsiveness", minval=0.4, maxval=2.5, step=0.05, group=groupSmooth, tooltip="Higher = faster; lower = smoother.")

groupSignals = "Signals"
showSync = input.bool(true, "Show BUY / SELL in pane", group=groupSignals, tooltip="Show small BUY/SELL markers in the oscillator pane.")
syncWindow = input.int(5, "BUY / SELL window (bars)", minval=0, maxval=10, group=groupSignals, tooltip="Maximum bars between the two zone exits. Default: 5.")
showCorrection = input.bool(true, "Show CORR BUY / SELL", group=groupSignals, tooltip="Show correction signals after a strong divergence.")
divValidity = input.int(25, "DIV setup window (bars)", minval=0, maxval=50, group=groupSignals, tooltip="Bars before/after DIV to find a yellow-line zone exit. Default: 25.")
corrWindow = input.int(25, "CORR confirmation window (bars)", minval=0, maxval=50, group=groupSignals, tooltip="Max bars after the yellow-line exit to confirm CORR.")
// CORR price-direction confirmation is always enabled (not a user setting).
minGapReduction = input.float(1.0, "CORR gap reduction (%)", minval=0.1, maxval=20.0, step=0.1, group=groupSignals, tooltip="Early CORR: minimum gap shrinkage. Two matching band exits also confirm CORR without this filter.")

groupDiv = "Divergence"
showDiv = input.bool(true, "Show DIV markers", group=groupDiv, tooltip="Show DIV markers in the oscillator pane.")
minStrength = input.float(9.0, "Minimum divergence strength (1–10)", minval=1.0, maxval=10.0, step=0.1, group=groupDiv, tooltip="DIV threshold, 1–10 in 0.1 steps. Higher = fewer signals.")

groupUI = "Display"
showChartZones = input.bool(true, "Show chart zones", group=groupUI, tooltip="Show the yellow line’s adaptive bands.")
showCompZones = input.bool(true, "Show comparison zones", group=groupUI, tooltip="Show the blue line’s adaptive bands.")
showPanel = input.bool(true, "Show comparison table", group=groupUI, tooltip="Show compared instruments and divergence strength.")
tablePosition = input.string("Top Right", "Table position", options=["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group=groupUI, tooltip="Choose a corner for the comparison table.")
showPriceSync = input.bool(true, "Show BUY / SELL on price chart", group=groupUI, tooltip="Also show BUY/SELL labels on the price chart.")
showPriceDiv = input.bool(false, "Show DIV markers on price chart", group=groupUI, tooltip="Also show DIV markers on the price chart.")

// ─── Fixed internal tuning: intentionally excluded from the simple menu ──────
int rangeLen = 25
int zoneLength = 70
float innerTail = 20.0
int divLookback = 500
int divExpandBars = 2
float divResetPct = 70.0

// ─── Comparison price ─────────────────────────────────────────────────────────
// Request only the selected basket. Pair closes and source timestamps are
// obtained together so each pair needs only one unique request context.
// Basket weights are user-defined (except the DXY-style USD basket).
// For inverse quoted crosses (e.g. EURCAD in a CAD-strength basket),
// logarithmic weights must be negative.
f_pair(string symbol) =>
    [pairClose, pairTime] = request.security(symbol, timeframe.period, [close, time], gaps=barmerge.gaps_off, lookahead=barmerge.lookahead_off, ignore_invalid_symbol=true)
    [pairClose > 0 ? math.log(pairClose) : na, pairTime]

float indexLog = na
int comparisonQuoteTime = na
float comparisonPrice = na
if indexMode == "Custom symbol"
    [customClose, customTime] = request.security(compareSymbol, timeframe.period, [close, time], gaps=barmerge.gaps_off, lookahead=barmerge.lookahead_off)
    comparisonPrice := customClose
    comparisonQuoteTime := customTime
else if indexMode == "Synthetic USD"
    [a, ta] = f_pair("OANDA:EURUSD")
    [b, tb] = f_pair("OANDA:USDJPY")
    [c, tc] = f_pair("OANDA:GBPUSD")
    [d, td] = f_pair("OANDA:USDCAD")
    [e, te] = f_pair("OANDA:USDSEK")
    [f, tf] = f_pair("OANDA:USDCHF")
    indexLog := -0.576*a + 0.136*b - 0.119*c + 0.091*d + 0.042*e + 0.036*f
    comparisonQuoteTime := math.min(math.min(math.min(ta, tb), math.min(tc, td)), math.min(te, tf))
else if indexMode == "Synthetic EUR"
    [a, ta] = f_pair("OANDA:EURUSD")
    [b, tb] = f_pair("OANDA:EURJPY")
    [c, tc] = f_pair("OANDA:EURGBP")
    [d, td] = f_pair("OANDA:EURCHF")
    [e, te] = f_pair("OANDA:EURCAD")
    [f, tf] = f_pair("OANDA:EURAUD")
    indexLog := 0.576*a + 0.145*b + 0.123*c + 0.058*d + 0.048*e + 0.050*f
    comparisonQuoteTime := math.min(math.min(math.min(ta, tb), math.min(tc, td)), math.min(te, tf))
else if indexMode == "Synthetic GBP"
    [a, ta] = f_pair("OANDA:GBPUSD")
    [b, tb] = f_pair("OANDA:EURGBP")
    [c, tc] = f_pair("OANDA:GBPJPY")
    [d, td] = f_pair("OANDA:GBPAUD")
    indexLog := 0.45*a - 0.30*b + 0.15*c + 0.10*d
    comparisonQuoteTime := math.min(math.min(ta, tb), math.min(tc, td))
else if indexMode == "Synthetic AUD"
    [a, ta] = f_pair("OANDA:AUDUSD")
    [b, tb] = f_pair("OANDA:AUDJPY")
    [c, tc] = f_pair("OANDA:EURAUD")
    [d, td] = f_pair("OANDA:AUDNZD")
    indexLog := 0.40*a + 0.25*b - 0.20*c + 0.15*d
    comparisonQuoteTime := math.min(math.min(ta, tb), math.min(tc, td))
else if indexMode == "Synthetic CAD"
    [a, ta] = f_pair("OANDA:USDCAD")
    [b, tb] = f_pair("OANDA:CADJPY")
    [c, tc] = f_pair("OANDA:EURCAD")
    indexLog := -0.60*a + 0.25*b - 0.15*c
    comparisonQuoteTime := math.min(ta, math.min(tb, tc))
else if indexMode == "Synthetic CHF"
    [a, ta] = f_pair("OANDA:USDCHF")
    [b, tb] = f_pair("OANDA:CHFJPY")
    [c, tc] = f_pair("OANDA:EURCHF")
    indexLog := -0.55*a + 0.25*b - 0.20*c
    comparisonQuoteTime := math.min(ta, math.min(tb, tc))
else if indexMode == "Synthetic JPY"
    [a, ta] = f_pair("OANDA:USDJPY")
    [b, tb] = f_pair("OANDA:EURJPY")
    [c, tc] = f_pair("OANDA:GBPJPY")
    indexLog := -0.40*a - 0.30*b - 0.30*c
    comparisonQuoteTime := math.min(ta, math.min(tb, tc))
else if indexMode == "Synthetic NZD"
    [a, ta] = f_pair("OANDA:NZDUSD")
    [b, tb] = f_pair("OANDA:NZDJPY")
    [c, tc] = f_pair("OANDA:AUDNZD")
    indexLog := 0.45*a + 0.30*b - 0.25*c
    comparisonQuoteTime := math.min(ta, math.min(tb, tc))
else if indexMode == "Synthetic XAU"
    [a, ta] = f_pair("OANDA:XAUUSD")
    [b, tb] = f_pair("OANDA:XAUEUR")
    [c, tc] = f_pair("OANDA:XAUJPY")
    [d, td] = f_pair("OANDA:XAUGBP")
    [e, te] = f_pair("OANDA:XAUAUD")
    [f, tf] = f_pair("OANDA:XAUCAD")
    [g, tg] = f_pair("OANDA:XAUCHF")
    indexLog := 0.50*a + 0.20*b + 0.10*c + 0.08*d + 0.05*e + 0.04*f + 0.03*g
    comparisonQuoteTime := math.min(math.min(math.min(ta, tb), math.min(tc, td)), math.min(math.min(te, tf), tg))
if indexMode != "Custom symbol"
    comparisonPrice := math.exp(indexLog)

// Data freshness: distinguish actual missing bars from an unchanged quoted price.
// A market can be closed, delayed or simply not publishing updates. Do not
// claim the exchange is closed when a feed alone cannot prove that.
// Compare bar timestamps on the *chart's* time axis, not against wall-clock time.
// This makes the same freshness rule usable on historical bars, live charts,
// and in Bar Replay, without a user setting or false historical NO DATA status.
// A forward-filled price is not evidence of a new trade/bar from the source.
int barMs = int(nz(timeframe.in_seconds(timeframe.period), 60)) * 1000
int allowedBarLagMs = math.max(barMs * 3, 60000)
bool compAligned = not na(comparisonPrice) and not na(comparisonQuoteTime) and time >= comparisonQuoteTime and time - comparisonQuoteTime <= allowedBarLagMs
bool chartAvailable = not na(close)
bool compAvailable = compAligned
bool marketsActive = chartAvailable and compAvailable
// Require two consecutive comparable bars before allowing a fresh signal.
bool signalDataReady = marketsActive and marketsActive[1]

// ─── Independent, causal two-pole adaptive filter (unchanged from Stretch v3) ─
f_alpha(float src) =>
    float path = 0.0
    for i = 1 to smoothLen
        path += math.abs(nz(src[i - 1], src) - nz(src[i], src))
    float net = math.abs(src - nz(src[smoothLen], src))
    float efficiency = path > 1e-10 ? math.min(net / path, 1.0) : 0.0
    float period = smoothLen * (1.25 - 0.4 * efficiency) / math.max(responsiveness, 0.25)
    math.min(0.45, math.max(0.03, 2.0 / (period + 1.0)))
f_pole(float src, float alpha) =>
    float s1 = na
    float s2 = na
    s1 := na(s1[1]) ? src : s1[1] + alpha * (src - s1[1])
    s2 := na(s2[1]) ? s1 : s2[1] + alpha * (s1 - s2[1])
    s2
f_position(float src, float alpha, float rawHigh, float rawLow) =>
    float priceLine = f_pole(src, alpha)
    float highLine = f_pole(rawHigh, alpha)
    float lowLine = f_pole(rawLow, alpha)
    float top = math.max(highLine, lowLine)
    float bottom = math.min(highLine, lowLine)
    float width = top - bottom
    not na(priceLine) and not na(width) and math.abs(width) > 1e-10 ? 100.0 * (priceLine - bottom) / width : na
float chartAlpha = f_alpha(close)
float compAlpha = f_alpha(comparisonPrice)
float chartHighRaw = ta.highest(close, rangeLen)
float chartLowRaw = ta.lowest(close, rangeLen)
float compHighRaw = ta.highest(comparisonPrice, rangeLen)
float compLowRaw = ta.lowest(comparisonPrice, rangeLen)
float chartOsc = f_position(close, chartAlpha, chartHighRaw, chartLowRaw)
float compRawOsc = f_position(comparisonPrice, compAlpha, compHighRaw, compLowRaw)

// ─── Comparison orientation: always keep chart oscillator price-aligned ───────
string[] tickerParts = str.split(compareSymbol, ":")
string selectedTicker = array.get(tickerParts, array.size(tickerParts) - 1)
string tickerUpper = str.upper(selectedTicker)
bool syntheticSelected = indexMode != "Custom symbol"
string syntheticCurrency = syntheticSelected ? str.substring(indexMode, 10, str.length(indexMode)) : ""
bool compareIsIndex = not syntheticSelected and (str.contains(tickerUpper, "DXY") or str.contains(tickerUpper, "USDOLLAR"))
bool compareUsdBase = str.startswith(tickerUpper, "USD") and not str.startswith(tickerUpper, "USDT")
bool compareUsdQuote = str.endswith(tickerUpper, "USD") and not compareUsdBase
bool chartUsdBase = syminfo.basecurrency == "USD"
bool chartUsdQuote = syminfo.currency == "USD" and not chartUsdBase
// Align a strength index with the base currency; invert it when the currency
// is the quote. The USD/DXY fallback also handles XAUUSD and BTCUSD.
bool syntheticBase = syntheticSelected and syminfo.basecurrency == syntheticCurrency
bool syntheticQuote = syntheticSelected and syminfo.currency == syntheticCurrency and not syntheticBase
bool autoInvert = syntheticSelected ? syntheticQuote : compareIsIndex ? not chartUsdBase : (compareUsdBase and chartUsdQuote) or (compareUsdQuote and chartUsdBase)
bool invertComp = invertMode == "Inverted" or (invertMode == "Auto" and autoInvert)
float compOsc = invertComp ? 100.0 - compRawOsc : compRawOsc

// ─── Independent moving zones, unchanged from Stretch v3 ────────────────────
float chartLowZone = ta.percentile_linear_interpolation(chartOsc, zoneLength, innerTail)
float chartHighZone = ta.percentile_linear_interpolation(chartOsc, zoneLength, 100.0 - innerTail)
float compLowZone = ta.percentile_linear_interpolation(compOsc, zoneLength, innerTail)
float compHighZone = ta.percentile_linear_interpolation(compOsc, zoneLength, 100.0 - innerTail)

// ─── Ordinary B/S: CHART line exit anchors a symmetric confirmation window ──
// The chart line can cross first or second. A comparison-line exit N bars
// earlier confirms on the chart crossing bar. Otherwise wait up to N bars
// for the comparison line. The chart must remain outside its entry zone.
// No future-bar lookahead and no retrospective label placement.
bool chartExitLow = signalDataReady and ta.crossover(chartOsc, chartLowZone) and chartOsc > chartOsc[1]
bool compExitLow = signalDataReady and ta.crossover(compOsc, compLowZone) and compOsc > compOsc[1]
bool chartExitHigh = signalDataReady and ta.crossunder(chartOsc, chartHighZone) and chartOsc < chartOsc[1]
bool compExitHigh = signalDataReady and ta.crossunder(compOsc, compHighZone) and compOsc < compOsc[1]
int sinceChartLow = nz(ta.barssince(chartExitLow), 100000)
int sinceCompLow = nz(ta.barssince(compExitLow), 100000)
int sinceChartHigh = nz(ta.barssince(chartExitHigh), 100000)
int sinceCompHigh = nz(ta.barssince(compExitHigh), 100000)
// Each oscillator rearms after revisiting ITS OWN zone, independently.
// They do not have to be oversold/overbought on the same candle.
var bool chartLowVisited = true
var bool compLowVisited = true
var bool chartHighVisited = true
var bool compHighVisited = true
var int pendingSyncBuyBar = na
var int pendingSyncSellBar = na
if barstate.isconfirmed
    if not marketsActive
        // Invalidate both the pending exits and the old market cycle.
        chartLowVisited := false
        compLowVisited := false
        chartHighVisited := false
        compHighVisited := false
        pendingSyncBuyBar := na
        pendingSyncSellBar := na
    else
        if chartOsc <= chartLowZone
            chartLowVisited := true
        if compOsc <= compLowZone
            compLowVisited := true
        if chartOsc >= chartHighZone
            chartHighVisited := true
        if compOsc >= compHighZone
            compHighVisited := true
        if not na(pendingSyncBuyBar) and (bar_index - pendingSyncBuyBar > syncWindow or chartOsc <= chartLowZone)
            pendingSyncBuyBar := na
        if not na(pendingSyncSellBar) and (bar_index - pendingSyncSellBar > syncWindow or chartOsc >= chartHighZone)
            pendingSyncSellBar := na
        if chartExitLow and chartLowVisited
            pendingSyncBuyBar := bar_index
        if chartExitHigh and chartHighVisited
            pendingSyncSellBar := bar_index
// The second exit can occur on either instrument; never look ahead.
// Historical second exits are valid only while the first remains outside.
bool blueAlreadyUp = sinceCompLow <= syncWindow and compOsc > compLowZone
bool blueAlreadyDown = sinceCompHigh <= syncWindow and compOsc < compHighZone
bool blueCatchesBuy = compExitLow and not na(pendingSyncBuyBar) and bar_index - pendingSyncBuyBar <= syncWindow
bool blueCatchesSell = compExitHigh and not na(pendingSyncSellBar) and bar_index - pendingSyncSellBar <= syncWindow
bool syncBuy = barstate.isconfirmed and signalDataReady and chartLowVisited and compLowVisited and chartOsc > chartLowZone and compOsc > compLowZone and ((chartExitLow and blueAlreadyUp) or blueCatchesBuy)
bool syncSell = barstate.isconfirmed and signalDataReady and chartHighVisited and compHighVisited and chartOsc < chartHighZone and compOsc < compHighZone and ((chartExitHigh and blueAlreadyDown) or blueCatchesSell) and not syncBuy
if syncBuy
    // Both need to revisit their respective LOWER zones before another BUY.
    chartLowVisited := false
    compLowVisited := false
    pendingSyncBuyBar := na
if syncSell
    // Both need to revisit their respective UPPER zones before another SELL.
    chartHighVisited := false
    compHighVisited := false
    pendingSyncSellBar := na

// ─── Always-on divergence strength and current correlation behaviour ─────────
float signedGap = chartOsc - compOsc
float gap = math.abs(signedGap)
float gapRank = ta.percentrank(gap, divLookback)
// Percent rank 91 => 9.1/10, rank 95 => 9.5/10. Round to tenths.
float stretchScore = na(gapRank) ? na : math.max(1.0, math.min(10.0, math.round(gapRank / 10.0, 1)))
float resetThreshold = ta.percentile_linear_interpolation(gap[1], divLookback, divResetPct)
bool validGap = not na(stretchScore) and not na(resetThreshold)
bool expanding = gap > gap[1]
for k = 1 to divExpandBars - 1
    expanding := expanding and gap[k] > gap[k + 1]
var bool divergenceEpisode = false
var bool watchOpen = false
var int divStartBar = na
// Reset divergence history and any existing confirmation after a data gap.
if barstate.isconfirmed and not marketsActive
    divergenceEpisode := false
    watchOpen := false
    divStartBar := na
// Normalization re-arms DIV but does NOT erase an existing CORR confirmation window.
if divergenceEpisode and (not validGap or gap <= resetThreshold)
    divergenceEpisode := false
bool divEvent = barstate.isconfirmed and signalDataReady and validGap and not divergenceEpisode and stretchScore >= minStrength and expanding
if divEvent
    divergenceEpisode := true
    watchOpen := true
    divStartBar := bar_index

// ─── CORR: independent DIV setup and confirmation windows ────────────────
// DIV starts the setup watch; the yellow-line band exit can be before or
// after DIV within the DIV setup window. CORR is confirmed by EITHER:
// (A) The gap contracts by the user-defined fraction after that yellow exit,
//     at any time within CORR confirmation window, OR
// (B) Both oscillators exit their OWN same-side bands within the shorter
//     BUY/SELL window. A late blue exit needs no additional gap contraction.
// For either path the chart line must remain outside its entry band;
// confirmation happens on the qualifying CLOSED bar, never backdated.
// A DIV watch persists after the gap has contracted (until its own expiry).
var int pendingCorrDir = 0     // 1 = potential BUY; -1 = potential SELL
var int pendingCorrBar = na
var float pendingStartGap = na
if barstate.isconfirmed and not marketsActive
    pendingCorrDir := 0
    pendingCorrBar := na
    pendingStartGap := na
int watchAge = na(divStartBar) ? 100000 : bar_index - divStartBar
int sinceUpperVisit = nz(ta.barssince(chartOsc >= chartHighZone), 100000)
int sinceLowerVisit = nz(ta.barssince(chartOsc <= chartLowZone), 100000)

// Only the setup window expires here. Existing candidates still get their
// own confirmation window after an accepted yellow-line exit.
if watchOpen and watchAge > divValidity
    watchOpen := false

// A new DIV starts a new setup and replaces any unconfirmed old candidate.
if divEvent
    pendingCorrDir := 0
    pendingCorrBar := na
    pendingStartGap := na
    // Look back for the chart's last *confirmed* exit from its own band.
    // Historical indexed gap anchors to the candle BEFORE that exit.
    int agoLow = sinceChartLow
    int agoHigh = sinceChartHigh
    // Do not revive an old opposite-direction exit after the chart has
    // subsequently visited the opposite extreme. The old exit is no longer
    // the current swing setup (e.g., old SELL while yellow is oversold).
    // These series are evaluated on every bar below, outside this branch.
    bool priorBuy = agoLow <= divValidity and agoLow <= agoHigh and sinceUpperVisit > agoLow
    bool priorSell = agoHigh <= divValidity and agoHigh < agoLow and sinceLowerVisit > agoHigh
    if priorBuy and chartOsc > chartLowZone and not na(gap[agoLow + 1])
        pendingCorrDir := 1
        pendingCorrBar := bar_index - agoLow
        pendingStartGap := gap[agoLow + 1]
    else if priorSell and chartOsc < chartHighZone and not na(gap[agoHigh + 1])
        pendingCorrDir := -1
        pendingCorrBar := bar_index - agoHigh
        pendingStartGap := gap[agoHigh + 1]

// A fresh chart-band exit during the DIV setup window supersedes an older
// opposite-direction candidate. Without this, a remembered old SELL could
// prevent a legitimate new BUY (and vice versa) for the entire watch.
if barstate.isconfirmed and signalDataReady and watchOpen and watchAge <= divValidity
    if chartExitLow and pendingCorrDir != 1
        pendingCorrDir := 1
        pendingCorrBar := bar_index
        pendingStartGap := gap[1]
    else if chartExitHigh and pendingCorrDir != -1
        pendingCorrDir := -1
        pendingCorrBar := bar_index
        pendingStartGap := gap[1]

int confirmationAge = na(pendingCorrBar) ? 100000 : bar_index - pendingCorrBar
bool corrPending = pendingCorrDir != 0 and not na(pendingStartGap) and pendingStartGap > 1e-10 and confirmationAge >= 0 and confirmationAge <= corrWindow
bool chartStillBuy = chartOsc > chartLowZone and chartOsc >= chartOsc[1] - 0.25
bool chartStillSell = chartOsc < chartHighZone and chartOsc <= chartOsc[1] + 0.25
bool bullPrice = close > close[1]
bool bearPrice = close < close[1]
float minContraction = pendingStartGap * minGapReduction / 100.0
bool enoughContraction = corrPending and gap <= pendingStartGap - minContraction and gap <= gap[1]
// Either line may exit first. Limit the TWO band exits to the ordinary
// synchronization window (5 by default), independently of the longer CORR
// confirmation window (25 by default).
int compLowExitBar = sinceCompLow < 100000 ? bar_index - sinceCompLow : na
int compHighExitBar = sinceCompHigh < 100000 ? bar_index - sinceCompHigh : na
bool sameSideBuy = corrPending and gap <= gap[1] and not na(compLowExitBar) and math.abs(compLowExitBar - pendingCorrBar) <= syncWindow and compOsc > compLowZone and chartOsc > chartLowZone
bool sameSideSell = corrPending and gap <= gap[1] and not na(compHighExitBar) and math.abs(compHighExitBar - pendingCorrBar) <= syncWindow and compOsc < compHighZone and chartOsc < chartHighZone
// Both-exit confirmation must not fire while the gap is still widening.
// The percentage threshold applies only to the early-contraction route.
// The earlier of the two possible confirmations triggers ONE CORR.
// Contraction requires the chart to be following through; a synchronized
// double-zone exit requires only that both remain outside their zones.
bool corrBuyConfirmed = (chartStillBuy and enoughContraction) or sameSideBuy
bool corrSellConfirmed = (chartStillSell and enoughContraction) or sameSideSell
// Immediate priority for an ordinary synchronized B/S after a recent DIV.
// No extra CORR candidate wait: check the gap and price direction on THIS bar.
// A stale opposite-direction candidate must never block a valid synchronized turn.
bool recentDivForSync = watchOpen and watchAge >= 0 and watchAge <= divValidity
bool instantCorrBuy = recentDivForSync and syncBuy and gap <= gap[1] and bullPrice
bool instantCorrSell = recentDivForSync and syncSell and gap <= gap[1] and bearPrice
bool correctionBuy = barstate.isconfirmed and signalDataReady and (instantCorrBuy or (corrPending and pendingCorrDir == 1 and corrBuyConfirmed and bullPrice))
bool correctionSell = barstate.isconfirmed and signalDataReady and (instantCorrSell or (corrPending and pendingCorrDir == -1 and corrSellConfirmed and bearPrice)) and not correctionBuy

if correctionBuy or correctionSell
    watchOpen := false
// Do not erase the joint-exit alternative merely because the yellow line
// paused for one bar. Cancel only if it actually re-enters its old zone.
bool chartExitInvalidated = pendingCorrDir == 1 ? chartOsc <= chartLowZone : pendingCorrDir == -1 ? chartOsc >= chartHighZone : false
if correctionBuy or correctionSell or chartExitInvalidated or confirmationAge > corrWindow
    pendingCorrDir := 0
    pendingCorrBar := na
    pendingStartGap := na

bool divLow = divEvent and signedGap < 0
bool divHigh = divEvent and signedGap >= 0

// ─── Visuals: preserve v3 labels/colors/sizes; no forced y-scale expansion ───
color chartColor = color.rgb(222, 173, 55)
color compColor = color.rgb(37, 169, 219)
color divOrange = color.rgb(255, 152, 0)
plot(marketsActive ? chartOsc : na, "Chart", color=chartColor, linewidth=2, style=plot.style_linebr)
plot(marketsActive ? compOsc : na, "Comparison", color=compColor, linewidth=2, style=plot.style_linebr)
plot(50, "50", color=color.new(color.gray, 85))
plot(showChartZones and marketsActive ? chartLowZone : na, "Chart low zone", color=color.new(color.lime, 65), style=plot.style_linebr)
plot(showChartZones and marketsActive ? chartHighZone : na, "Chart high zone", color=color.new(color.red, 65), style=plot.style_linebr)
plot(showCompZones and marketsActive ? compLowZone : na, "Comparison low zone", color=color.new(compColor, 70), style=plot.style_linebr)
plot(showCompZones and marketsActive ? compHighZone : na, "Comparison high zone", color=color.new(compColor, 70), style=plot.style_linebr)
// Original compact B/S labels in the pane; CORR takes priority on a shared bar.
plotshape(showSync and syncBuy and not correctionBuy, title="SYNC BUY", text="B", style=shape.labelup, location=location.bottom, color=color.new(color.green, 15), textcolor=color.white, size=size.tiny)
plotshape(showSync and syncSell and not correctionSell, title="SYNC SELL", text="S", style=shape.labeldown, location=location.top, color=color.new(color.red, 15), textcolor=color.white, size=size.tiny)
plotshape(showDiv and divLow, title="DIV LOW", text="DIV", style=shape.circle, location=location.bottom, color=color.new(divOrange, 80), textcolor=divOrange, size=size.tiny)
plotshape(showDiv and divHigh, title="DIV HIGH", text="DIV", style=shape.circle, location=location.top, color=color.new(divOrange, 80), textcolor=divOrange, size=size.tiny)
plotshape(showCorrection and correctionBuy, title="CORR BUY", text="C↑\nbuy", style=shape.circle, location=location.bottom, color=color.new(color.teal, 0), textcolor=color.white, size=size.tiny)
plotshape(showCorrection and correctionSell, title="CORR SELL", text="C↓\nsell", style=shape.circle, location=location.top, color=color.new(color.purple, 0), textcolor=color.white, size=size.tiny)
plotshape(showDiv and showPriceDiv and divLow, title="Chart DIV below", text="DIV", style=shape.circle, location=location.belowbar, color=color.new(divOrange, 80), textcolor=divOrange, size=size.tiny, force_overlay=true)
plotshape(showDiv and showPriceDiv and divHigh, title="Chart DIV above", text="DIV", style=shape.circle, location=location.abovebar, color=color.new(divOrange, 80), textcolor=divOrange, size=size.tiny, force_overlay=true)
plotshape(showPriceSync and showSync and syncBuy and not correctionBuy, title="Chart SYNC BUY", text="B", style=shape.labelup, location=location.belowbar, color=color.new(color.green, 15), textcolor=color.white, size=size.tiny, force_overlay=true)
plotshape(showPriceSync and showSync and syncSell and not correctionSell, title="Chart SYNC SELL", text="S", style=shape.labeldown, location=location.abovebar, color=color.new(color.red, 15), textcolor=color.white, size=size.tiny, force_overlay=true)
plotshape(showCorrection and correctionBuy, title="Chart CORR BUY", text="C↑\nbuy", style=shape.circle, location=location.belowbar, color=color.new(color.teal, 0), textcolor=color.white, size=size.tiny, force_overlay=true)
plotshape(showCorrection and correctionSell, title="Chart CORR SELL", text="C↓\nsell", style=shape.circle, location=location.abovebar, color=color.new(color.purple, 0), textcolor=color.white, size=size.tiny, force_overlay=true)

// ─── Table: two tickers and strength; replace strength when data is stale ──
string comparisonLabel = syntheticSelected ? indexMode : selectedTicker
string strengthLevel = na(stretchScore) ? "WARMUP" : stretchScore >= 9 ? "HIGH" : stretchScore >= 7 ? "ELEVATED" : "NORMAL"
string stretchText = na(stretchScore) ? "WARMUP" : str.tostring(stretchScore, "#.0") + "/10  " + strengthLevel
color stretchColor = na(stretchScore) ? color.silver : stretchScore >= 9 ? divOrange : stretchScore >= 7 ? color.orange : color.silver
color panelBg = color.new(color.rgb(28, 28, 28), 8)
var table panel = table.new(position.top_right, 2, 3, border_width=1, frame_color=color.new(color.gray, 40), border_color=color.new(color.gray, 55))
if barstate.islast
    table.set_position(panel, tablePosition == "Top Left" ? position.top_left : tablePosition == "Bottom Right" ? position.bottom_right : tablePosition == "Bottom Left" ? position.bottom_left : position.top_right)
    if showPanel
        string chartLabel = syminfo.ticker + (chartAvailable ? "" : "  NO RECENT BAR")
        string compareLabel = comparisonLabel + (invertComp ? "  inverted" : "") + (compAvailable ? "" : "  NO RECENT BAR")
        table.cell(panel, 0, 0, "●", text_color=chartAvailable ? chartColor : divOrange, bgcolor=panelBg, text_size=size.small)
        table.cell(panel, 1, 0, chartLabel, text_color=chartAvailable ? chartColor : divOrange, bgcolor=panelBg, text_size=size.small)
        table.cell(panel, 0, 1, "●", text_color=compAvailable ? compColor : divOrange, bgcolor=panelBg, text_size=size.small)
        table.cell(panel, 1, 1, compareLabel, text_color=compAvailable ? compColor : divOrange, bgcolor=panelBg, text_size=size.small)
        table.cell(panel, 0, 2, marketsActive ? "Divergence" : "Comparison", text_color=color.white, bgcolor=panelBg, text_size=size.small)
        table.cell(panel, 1, 2, marketsActive ? stretchText : "NO RECENT BAR", text_color=marketsActive ? stretchColor : divOrange, bgcolor=panelBg, text_size=size.small)
    else
        table.clear(panel, 0, 0, 1, 2)

// Alerts are based on signals, not whether a chart marker is hidden.
// CORR is triggered by either the gap-reduction or double-band-exit path.
// Existing five alert names and signal visuals remain unchanged.
alertcondition(showSync and syncBuy and not correctionBuy, "SYNC BUY", "SMT Divergence Oscillator [josseliani]: SYNC BUY {{ticker}} {{interval}}")
alertcondition(showSync and syncSell and not correctionSell, "SYNC SELL", "SMT Divergence Oscillator [josseliani]: SYNC SELL {{ticker}} {{interval}}")
alertcondition(showDiv and divEvent, "DIV", "SMT Divergence Oscillator [josseliani]: high divergence strength {{ticker}} {{interval}}")
alertcondition(showCorrection and correctionBuy, "CORR BUY", "SMT Divergence Oscillator [josseliani]: confirmed CORR BUY {{ticker}} {{interval}}")
alertcondition(showCorrection and correctionSell, "CORR SELL", "SMT Divergence Oscillator [josseliani]: confirmed CORR SELL {{ticker}} {{interval}}")
